package okw.communication;

import java.util.HashMap;
import java.util.Random;

import okw.utils.BeanProvider;

import org.jboss.remoting.CannotConnectException;
import org.jboss.remoting.ServerInvoker.InvalidStateException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

import core.communication.message.Direction;
import core.communication.message.Message;
import core.communication.message.NodeStateEnum;
import core.database.NodeType;

@Component
@Scope("prototype")
public class MessageSender implements ApplicationContextAware, Runnable {

	private static final Logger L = LoggerFactory.getLogger(MessageSender.class);

	private ApplicationContext context;

	private volatile Thread thread = null;
	private CommunicationService communicationService;
	private Random random = new Random();

	public MessageSender() {
		L.debug("Tworzę MessageSender");

		thread = new Thread(this, "sender");

		L.debug("Utworzono MessageSender");
	}

	public void stop() {
		L.debug("Stop: {} {}", thread.getName(), thread.getId());

		thread = null;
		wakeUp();
	}

	public synchronized void wakeUp() {
		L.debug("Wstaje: MessageSender");

		notify();
	}

	@Override
	public void run() {
		communicationService = context.getBean(CommunicationService.class);
		Message message = null;
		Client client = null;
		while (thread != null) {
			message = null;
			client = null;
			// Pobieramy wiadomość do wysłania
			L.debug("Pobieram wiadomość");
			message = communicationService.getMessageToSend();
			// Jeżeli nie ma wiadomości to czekamy
			if (message == null) {
				synchronized (this) {
					try {
						if (thread == null)
							return;
						L.debug("Nie ma wiadomości, usypiam");
						wait();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			} else {
				L.debug("Wysyłam " + message.getMessageType());
				// Pobieramy klienta ze ścieżki paczki
				L.debug("Pobieram klienta " + message.getURIFromPath());

				if (message.getDirection() == Direction.UP)
					client = communicationService.getUpperClients().get(communicationService.getPrefferedParentURI());
				else if (message.getDirection() == Direction.DOWN)
					client = communicationService.getLowerClients().get(message.getURIFromPath());

				if (client == null) {
					L.debug("Nie znaleziono klienta");
					communicationService.resendMessage(message);
				} else {
					L.debug("Pobrano klienta: {}", client.getLocatorURI());
					if (client.getIsWorking()) { // Jeżeli klient jest osiągalny
						// Próbujemy wysłać wiadomość
						try {
							L.debug("Próbuje wysłać " + message.getMessageType());
							BeanProvider.mainWindow().appendMessage(
									"Wysyłam wiadomość typu " + message.getMessageType() + " zaadresowaną do " + (message.getDestinationURI() != null ? message.getDestinationURI() : "serwera CKW")
											+ " przez węzeł " + client.getLocatorURI());
							client.send(message);
							communicationService.removeMessageToSend(message);
							// Nie możemy połączyć się z klientem
						} catch (CannotConnectException | InvalidStateException e) {
							L.debug("Brak połączenia z klientem " + client.getLocatorURI());
							BeanProvider.mainWindow().appendMessage(
									"Nie udało się wysłać wiadomość typu " + message.getMessageType() + " zaadresowanej do "
											+ (message.getDestinationURI() != null ? message.getDestinationURI() : "serwera CKW") + " przez węzeł " + client.getLocatorURI());
							BeanProvider.mainWindow().appendMessage("Węzeł " + client.getLocatorURI() + " nieosiągalny");
							// Oznaczamy klienta jako nieosiągalnego i
							// uruchamiamy dla niego wątek sprawdzający jego
							// stan
							if (thread == null)
								return;
							checkClient(client);
							resend(message, client);
						} catch (Throwable e) {
							if (thread == null)
								return;
							e.printStackTrace();
						}
					} else
						resend(message, client);
				}
			}
		}
	}

	private void checkClient(Client client) {
		client.setIsWorking(false);
		communicationService.sendNodeStateMessage(client.getLocatorURI(), NodeStateEnum.OFF);
		communicationService.addClientToCheck(client);
	}

	private void resend(Message message, Client client) {
		L.debug("Wysyłam ponownie wiadomość " + message.getMessageType() + " na kliencie " + client.getLocatorURI());
		if (message.getDirection() == Direction.UP) {
			sendRandom(message);
		} else {
			if (client.getLocatorURI().compareTo(message.getDestinationURI()) == 0)
				communicationService.resendMessage(message);
			else
				sendRandom(message);
		}
	}

	private void sendRandom(Message message) {
		Client client = null;
		HashMap<String, Client> clients = new HashMap<String, Client>();
		if (message.getDirection() == Direction.UP)
			clients = communicationService.getUpperClients();
		else if (message.getDirection() == Direction.DOWN)
			clients = communicationService.getLowerClients();

		// Jeżeli mamy tylko jednego klienta to wrzucamy wiadomość z
		// powrotem na kolejkę bo nie ma innych klientów, którzy mogliby ją
		// przyjąć
		if (clients.size() <= 1) {
			L.debug("Mniej niż 2 klientów, nie można wysłać losowo, wysyłam ponownie");
			communicationService.resendMessage(message);
		} else {
			// Pobieramy losowego klienta
			client = (Client) clients.values().toArray()[random.nextInt(clients.size())];
			L.debug("Pobrano losowego klienta {}", client.getLocatorURI());
			if (client.getType() == NodeType.OKW) {
				// Sprawdzamy czy klient jest osiągalny
				if (client.getIsWorking()) {
					try {
						if (message.getDirection() == Direction.DOWN)
							message.updatePath(client.getLocatorURI());
						L.debug("Próbuje wysłać " + message.getMessageType());
						BeanProvider.mainWindow().appendMessage(
								"Wysyłam wiadomość typu " + message.getMessageType() + " zaadresowaną do " + (message.getDestinationURI() != null ? message.getDestinationURI() : "serwera CKW")
										+ " przez węzeł " + client.getLocatorURI());
						client.send(message);
						communicationService.removeMessageToSend(message);
						// Nie możemy połączyć się z klientem
					} catch (CannotConnectException | InvalidStateException e) {
						BeanProvider.mainWindow().appendMessage(
								"Nie udało się wysłać wiadomość typu " + message.getMessageType() + " zaadresowanej do "
										+ (message.getDestinationURI() != null ? message.getDestinationURI() : "serwera CKW") + " przez węzeł " + client.getLocatorURI());
						BeanProvider.mainWindow().appendMessage("Węzeł " + client.getLocatorURI() + " nieosiągalny");
						L.debug("Brak połączenia z klientem " + client.getLocatorURI());
						if (thread == null)
							return;
						checkClient(client);
						sendRandom(message);
					} catch (Throwable e) {
						if (thread == null)
							return;
						e.printStackTrace();
					}
				} else
					communicationService.resendMessage(message);
			} else
				communicationService.resendMessage(message);
		}
	}

	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		context = applicationContext;
	}

}
